﻿namespace UDKDevKitVSX.Concrete
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.IO;
    using Microsoft.VisualStudio.Debugger.Interop;
    using UDKDevKitVSXDebugEngine.Definitions;
    using UDKDevKitVSXDebugEngine.Exceptions;
    using UDKDevKitVSXDebugEngine.Utils;

    // This class represents a document context to the debugger. A document context represents a location within a source file. 
    internal class DocumentContext : IDebugDocumentContext2
    {
        private string fileName;
        private string className;
        private TEXT_POSITION begPos;
        private TEXT_POSITION endPos;
        private uint[] linesColumnBeginsMetrix;
        private uint[] linesColumnEndsMetrix;
        private uint currentStatementLine;

        public DocumentContext(string className, string fileName, TEXT_POSITION begPos, TEXT_POSITION endPos, uint[] linesColumnBeginsMetrix, uint[] linesColumnEndsMetrix)
        {
            if (String.IsNullOrEmpty(fileName))
            {
                Debug.Fail("ArgumentException - fileName");
                throw new ArgumentException("linesMetrix");
            }

            if (linesColumnEndsMetrix == null)
            {
                Debug.Fail("ArgumentNullException - linesMetrix");
                throw new ArgumentNullException("linesMetrix");
            }

            this.className = className;
            this.fileName = fileName;
            this.begPos = begPos;
            this.endPos = endPos;
            this.linesColumnBeginsMetrix = linesColumnBeginsMetrix;
            this.linesColumnEndsMetrix = linesColumnEndsMetrix;
        }

        public DocumentContext(string className, string fileName, TEXT_POSITION begPos, TEXT_POSITION endPos)
        {
            if (String.IsNullOrEmpty(fileName))
            {
                Debug.Fail("ArgumentException - fileName");
                throw new ArgumentException("linesMetrix");
            }

            this.fileName = fileName;
            this.className = className;
            this.begPos = begPos;
            this.endPos = endPos;

            GetFileLinesMetrix(this.fileName, out this.linesColumnBeginsMetrix, out this.linesColumnEndsMetrix);
        }

        public string ClassName
        {
            get { return this.className; }
        }

        public string FileName
        {
            get { return this.fileName; }
        }

        public uint CurrentDebuggingStatementLine
        {
            get { return this.currentStatementLine; }
            set { this.currentStatementLine = value; }
        }

        public static DocumentContext Create(string fileName, string className)
        {
            if (String.IsNullOrEmpty(fileName))
            {
                Debug.Fail("ArgumentException - fileName");
                throw new ArgumentException("linesMetrix");
            }

            if (!File.Exists(fileName))
            {
                Debug.Fail(String.Concat("FileNotFoundException - ", fileName));
                throw new FileNotFoundException(fileName);
            }

            uint[] linesBeginColumns;
            uint[] linesEndColumns;
            var lines = GetFileLinesMetrix(fileName, out linesBeginColumns, out linesEndColumns);

            TEXT_POSITION begPos = new TEXT_POSITION()
            {
                dwLine = 0,
                dwColumn = 0
            };

            TEXT_POSITION endPos = new TEXT_POSITION()
            {
                dwLine = lines,
                dwColumn = 0
            };

            return new DocumentContext(className, fileName, begPos, endPos, linesBeginColumns, linesEndColumns);
        }

        private static uint GetFileLinesMetrix(string fileName, out uint[] linesBeginsMetrix, out uint[] linesEndsMetrix)
        {
            uint lines = 0;

            Debug.WriteLine(String.Concat("LoadDocumentFile: Opening file: ", fileName));

            var streamReader = new StreamReader(fileName);
            var linesBeginsColumns = new List<uint>();
            var linesEndsColumns = new List<uint>();

            lines = 0;
            while (!streamReader.EndOfStream)
            {
                string line = streamReader.ReadLine();
                lines++;

                // fin first character which is not space
                var i = 0;
                while (i < line.Length && (line[i] == ' ' || line[i] == '\t' || line[i] == '\v')) i++;

                linesBeginsColumns.Add((uint)i);
                linesEndsColumns.Add((uint)line.Length);
            }

            linesBeginsMetrix = linesBeginsColumns.ToArray();
            linesEndsMetrix = linesEndsColumns.ToArray();

            return lines;
        }

        public bool IsInReange(uint line)
        {
            return this.begPos.dwLine <= line && line <= this.endPos.dwLine;
        }

        #region IDebugDocumentContext2 Members

        // Compares this document context to a given array of document contexts.
        int IDebugDocumentContext2.Compare(enum_DOCCONTEXT_COMPARE Compare, IDebugDocumentContext2[] rgpDocContextSet, uint dwDocContextSetLen, out uint pdwDocContext)
        {
            dwDocContextSetLen = 0;
            pdwDocContext = 0;

            return Constants.E_NOTIMPL;
        }

        // Retrieves a list of all code contexts associated with this document context.
        // The engine sample only supports one code context per document context and 
        // the code contexts are always memory addresses.
        int IDebugDocumentContext2.EnumCodeContexts(out IEnumDebugCodeContexts2 ppEnumCodeCxts)
        {
            ppEnumCodeCxts = null;
            return Constants.E_NOTIMPL;
        }

        // Gets the document that contains this document context.
        // This method is for those debug engines that supply documents directly to the IDE. Since UDK Script Debug Engine
        // does not do this, this method returns E_NOTIMPL.
        int IDebugDocumentContext2.GetDocument(out IDebugDocument2 ppDocument)
        {
            ppDocument = null;
            return Constants.E_NOTIMPL;
        }

        // Gets the language associated with this document context.
        // The language for this sample is always C++
        int IDebugDocumentContext2.GetLanguageInfo(ref string pbstrLanguage, ref Guid pguidLanguage)
        {
            pbstrLanguage = "Unreal Script";
            pguidLanguage = Guids.guidLanguageUnrealScript;
            return Constants.S_OK;
        }

        // Gets the displayable name of the document that contains this document context.
        int IDebugDocumentContext2.GetName(enum_GETNAME_TYPE gnType, out string pbstrFileName)
        {
            pbstrFileName = fileName;
            return Constants.S_OK;
        }

        // Gets the source code range of this document context.
        // A source range is the entire range of source code, from the current statement back to just after the previous s
        // statement that contributed code. The source range is typically used for mixing source statements, including 
        // comments, with code in the disassembly window.
        // Sincethis engine does not support the disassembly window, this is not implemented.
        int IDebugDocumentContext2.GetSourceRange(TEXT_POSITION[] pBegPosition, TEXT_POSITION[] pEndPosition)
        {
            try
            {
                pBegPosition[0] = begPos;
                pEndPosition[0] = endPos;
            }
            catch (ComponentException e)
            {
                return e.HResult;
            }
            catch (Exception e)
            {
                return Core.UnexpectedException(e);
            }

            return Constants.S_OK;
        }

        // Gets the file statement range of the document context.
        // A statement range is the range of the lines that contributed the code to which this document context refers.
        int IDebugDocumentContext2.GetStatementRange(TEXT_POSITION[] pBegPosition, TEXT_POSITION[] pEndPosition)
        {
            try
            {
                pBegPosition[0].dwColumn = this.currentStatementLine < linesColumnBeginsMetrix.Length ? linesColumnBeginsMetrix[this.currentStatementLine] : 0;
                pBegPosition[0].dwLine = this.currentStatementLine;

                pEndPosition[0].dwColumn = this.currentStatementLine < linesColumnEndsMetrix.Length ? linesColumnEndsMetrix[this.currentStatementLine] : 0;
                pEndPosition[0].dwLine = this.currentStatementLine;
            }
            catch (ComponentException e)
            {
                return e.HResult;
            }
            catch (Exception e)
            {
                return Core.UnexpectedException(e);
            }

            return Constants.S_OK;
        }

        // Moves the document context by a given number of statements or lines.
        // This is used primarily to support the Autos window in discovering the proximity statements around 
        // this document context. 
        int IDebugDocumentContext2.Seek(int nCount, out IDebugDocumentContext2 ppDocContext)
        {
            ppDocContext = null;
            return Constants.E_NOTIMPL;
        }

        #endregion

        public static string GetClassNameFromFilePath(string filePath)
        {
            int len = "\\Classes\\".Length;
            int pos = filePath.IndexOf("\\Classes\\");
            if (pos == -1)
            {
                return filePath;
            }

            string packagePath = filePath.Substring(0, pos);
            string fileName = filePath.Substring(pos + len, filePath.Length - len - pos - ".uc".Length);

            string packageName = String.Empty;

            int posFolder = packagePath.LastIndexOf("\\");
            if (posFolder != -1)
            {
                packageName = packagePath.Substring(posFolder + 1, packagePath.Length - posFolder - 1);
            }

            return String.Concat(packageName, '.', fileName);
        }
    }
}
